package com.chris.core.socket.bio.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 多线程，同步阻塞式 -> sokect 客户端
 * 抽象类
 * 苏斌 2022-3-20
 */
public abstract class BioSokcetClientUtil {

    //#region 参数及变量
    private Logger log = LoggerFactory.getLogger(this.getClass());
    //坑，慎用Executors ， 此对象不会消亡，对象即使null,内存里面仍然有这个线程池，池本身不会GC，如果存在多线程对象都有线程池，多线程释放，线程池不释放， 线程会爆
    private ScheduledExecutorService worker = Executors.newSingleThreadScheduledExecutor(); //Executors.newScheduledThreadPool(1);
    private String host;

    private int port;

    /**
     * 消息断言
     */
    private String breakMark = "\r\n";// \n    \r\n    ~!@#bbq8$%^&*\n;

    /**
     * 回车  chr(13) \r
     * readline 是否包含,结合 breakMark 一起判断
     */
    private String _CR="\r";

    /**
     * 回车  chr(10) \n
     * readline 是否包含,结合 breakMark 一起判断
     */
    private String _LF="\n";

    /**
     * 当失去连接时，是否自动
     */
    private boolean isAutoConnected = false;

    /**
     * 读取超时
     */
    private int readTimeout=180000;

    Socket socket = null;//new Socket("127.0.0.1",4700);

    /**
     * 上传文件夹
     */
    private String uploadFilePath="./uploads/";

    public String getUploadFilePath() {
        return uploadFilePath;
    }

    public void setUploadFilePath(String uploadFilePath) {
        this.uploadFilePath = uploadFilePath;
    }


    //#endregion

    //#region 初始化系统

    /**
     * 构造函数
     *
     * @param host 服务端-》IP地址
     * @param port 服务端-》端口
     */
    public BioSokcetClientUtil(String host, int port) throws IOException {
        this.host=host;
        this.port=port;

    }

    /**
     * 构造函数
     *
     * @param host            服务端-》IP地址
     * @param port            服务端-》端口
     * @param readTimeout            读写超时
     * @param isAutoConnected 是否自动重连接
     * @param breakMark       消息断言 默认 \r\n , 如果为空，即是不间断读写，有数据就抛出
     *                        * @throws IOException
     * @throws IOException
     */
    public BioSokcetClientUtil(String host, int port,int readTimeout, boolean isAutoConnected, String breakMark) throws IOException {
        this.host=host;
        this.port=port;
        this.readTimeout=readTimeout;
        this.isAutoConnected=isAutoConnected;
        this.breakMark=breakMark;

    }

    /**
     * 初始化系统
     *
     */
    private void IntSystem() {
        if (breakMark.endsWith("\r\n"))
        {
            _CR="\r";
            _LF="\n";
        }
        else if (breakMark.endsWith("\n"))
        {
            _CR="";
            _LF="\n";
        }
        else
        {
            //目前采用readline方式,此处会出错
            //2022-7-13修正,可接收不间段数据流
            _CR="";
            _LF="";
        }

    }

    public void Open() {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                do {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    IntSystem();
                    connect();
                    DoReviceMsg();
                }
                while (isAutoConnected);
            }
        });
        t.start();
    }

    /**
     * 建立链接
     *
     * @throws IOException
     */
    private void connect() {
        try {
            socket = new Socket();

            //读取等待超时，超过30秒没有数据流自动断开,要设置心跳
            socket.setSoTimeout(readTimeout);//60000
            /*
            接收缓冲区buffer满之后的处理策略

接收缓冲区被TCP和UDP用来缓存网络上来的数据，一直保存到应用进程读走为止。

TCP
对于TCP，如果应用进程一直没有读取，buffer满了之后，发生的动作是：通知对端TCP协议中的窗口关闭。这个便是滑动窗口的实现。
保证TCP套接口接收缓冲区不会溢出，从而保证了TCP是可靠传输。因为对方不允许发出超过所通告窗口大小的数据。 这就是TCP的流量控制，如果对方无视窗口大小而发出了超过窗口大小的数据，则接收方TCP将丢弃它。
UDP
当套接口接收缓冲区满时，新来的数据报无法进入接收缓冲区，此数据报就被丢弃。UDP是没有流量控制的；快的发送者可以很容易地就淹没慢的接收者，导致接收方的UDP丢弃数据报。
发送缓冲区
如何使用发送缓冲区

进程调用send发送的数据的时候，最简单情况（也是一般情况），将数据拷贝进入socket的内核发送缓冲区之中，然后send便会在上层返回。
换句话说，send返回之时，数据不一定会发送到对端去（和write写文件有点类似），send仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中。
每个UDP socket都有一个接收缓冲区，没有发送缓冲区，从概念上来说就是只要有数据就发，不管对方是否可以正确接收，所以不缓冲，不需要发送缓冲区。
             */
            //流量控制 不管发送端和接收端缓冲区的大小如何设置，最后生效的是两者之间值最小的那个大小
            socket.setReceiveBufferSize(socket.getReceiveBufferSize() * 2);
            socket.setSendBufferSize(socket.getSendBufferSize() * 2);
            socket.setKeepAlive(true);
            socket.connect(new InetSocketAddress(host, port), 30000);

        } catch (IOException e) {
            log.error("连接服务端失败:" + e.toString());
            // e.printStackTrace();
        }
    }

    /**
     * 客服端关闭链接
     */
    public void Stop() {
        if (socket != null) {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            // socketMap.remove(dto);
            // subOnlineCount();
            log.debug("客服端关闭socket连接:");
        }
    }


    //#endregion

    //#region 接收内容

    /**
     * 循环接收消息(文本）
     *
     * @throws IOException
     */
    private void DoReviceMsg() {
       // if (socket == null)
        //    return;
        InputStream inputStream = null;
        BufferedReader bufferedReader = null;
        try {

            //-----------方法1-------------
            inputStream = socket.getInputStream();
            //----------方法1-------------

            //循环读取客服端发送数据
            if (_CR=="" && _LF=="") {
                //没有断言情况，不间断读写 ，有数据流就抛出 2022-7-13
                byte[] bytes = new byte[1024];
                int len;
                StringBuilder sb = new StringBuilder();
                //只有当客户端关闭它的输出流的时候，服务端才能取得结尾的-1
                while ((len = inputStream.read(bytes)) != -1) {
                    // 注意指定编码格式，发送方和接收方一定要统一，建议使用UTF-8
                    sb.append(new String(bytes, 0, len, "UTF-8"));
                    OnRevice(sb.toString());
                }
            }
            else {
                //由Socket对象得到输入流，并构造相应的BufferedReader对象
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
                //临时读入文本
                String readline;
                //文本缓冲
                StringBuilder stringBuilder = new StringBuilder();
                //循环读取客服端发送数据
                do {
                    // log.debug("等待读取数据，阻塞 线程:" + Thread.currentThread().getId() + " " + Thread.currentThread().getName());
                    //读取一行文本。换行符（'\n'）、回车符（'\r'）或紧跟换行符的回车符中的任何一个都会终止换行。
                    readline = bufferedReader.readLine(); //从系统标准输入读入一字符串
                    if (readline != null) {
                        readline += _CR + _LF;//readLine()去掉了原始\n 或\r\n ,重新加上恢复原始字符串内容
                        if (readline.equals("!@#$%cmd:file:begin^&*" + breakMark)) {
                            log.debug("截获上传文件命令，转跳上传文件");
                            //第二行读取文件名
                            String fileName = delSuffixMsg(bufferedReader.readLine() + _CR + _LF);
                            String str = "";
                            //第三行读取文件大小
                            str = delSuffixMsg(bufferedReader.readLine() + _CR + _LF);
                            Long fileSize = Long.parseLong(str);
                            //第四行开始接收文件
                            DoReviceFile(fileName, fileSize, socket);
                            continue;//继续下一个循环获取消息
                        }

                        if (readline.endsWith(breakMark))//注意，如果参数是空字符串，或者等于此 String 对象（用 equals(Object) 方法确定），则结果为 true
                        {
                            //如果存在断言-->单独一行内有断言，为之结束
                            readline = delSuffixMsg(readline);
                            stringBuilder.append(readline);
                            if (stringBuilder.length() > 0) {
                                OnRevice(stringBuilder.toString());
                                stringBuilder.setLength(0);
                            }
                        } else {
                            //没有断言， 当作内容添加进stringBuilder
                            stringBuilder.append(readline);
                        }

                    }

                }
                while (readline != null);//有坑！！！！！===》 readLine 必须是有 \n  才会读取完毕
            }
            //----------方法2-------------
            /*
            byte[] bytes = new byte[1024];
            int len;
            StringBuilder sb = new StringBuilder();
            //只有当客户端关闭它的输出流的时候，服务端才能取得结尾的-1
            while ((len = inputStream.read(bytes)) != -1) {
                // 注意指定编码格式，发送方和接收方一定要统一，建议使用UTF-8
                sb.append(new String(bytes, 0, len, "UTF-8"));
            }
            System.out.println("get message from client: " + sb);

             */
            //----------方法3------------
            /*
            //注意此方法有限制：用dataOutputStream写入，要用 DataInputStream 读取，因为
            //dataOutputStream 写入内容，带有文件类型识别前缀，需要DataInputStream识别
            inputStream = dto.getSocket().getInputStream();
            DataInputStream dataInputStream = new DataInputStream(inputStream);
            //由Socket对象得到输入流，并构造相应的BufferedReader对象
            //临时读入文本
            String readline;

            //循环读取客服端发送数据
            do {
                // log.debug("等待读取数据，阻塞 线程:" + Thread.currentThread().getId() + " " + Thread.currentThread().getName());
                readline = dataInputStream.readUTF(); //从系统标准输入读入一字符串
                if (readline != null) {
                    OnRevice(dto.getCid(), readline);
                }
            }
            while (readline != null && !readline.equals(breakMark));//有坑！！！！！===》 readLine 必须是有 \n  才会读取完毕
            */
            //------------------------

        } catch (Exception e) {
            log.error("客服端读取内容断开:" + e.toString());
            // e.printStackTrace();//出错，打印出错信息
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        if (bufferedReader != null) {
            try {
                bufferedReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (socket != null) {
            try {
                socket.close();
                socket = null;
                log.debug("客服端socket连接关闭");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * 接收文件
     * 仅获取上传文件动作时候可用
     * @param fileName
     * @param socket
     */
    private void DoReviceFile(String fileName,Long fileSize,Socket socket)
    {
        FileOutputStream fos=null;
        DataInputStream  dis=null;
        try {
            dis = new DataInputStream(socket.getInputStream());
            // 文件名和长度
            // String fileName = dis.readUTF();
            //readLong有问题，亏阻断，原因不明
            // long fileLength = dis.readLong();
            log.debug("文件的长度为:" + fileSize);
            File directory = new File(uploadFilePath);
            if(!directory.exists()) {
                directory.mkdir();
            }
            File file = new File(directory.getAbsolutePath() + File.separatorChar + fileName);
            if (file!=null && file.isFile() && file.exists() )
                file.delete();
            fos = new FileOutputStream(file);
            log.debug("file。。。。。。。。。。。。。。"+file);
            log.debug("fileName。。。。。。。。。。。。。。"+fileName);

            log.debug("======== 开始接收文件 ========");
            byte[] bytes = new byte[1024];
            int length = 0;
            long passedlen=0;
            Long per=0L;
            //统计开始时间
            long beginTime=System.currentTimeMillis();
            //注意坑，不能做死循环 =-1 ，因为从 外部while调入进来的，内部死循环出不去
            //外部就永远收不到消息了，只能定长读取内容
            while((length = dis.read(bytes, 0, bytes.length)) != -1) {

                fos.write(bytes, 0, length);
                fos.flush();
                passedlen+=length;
                if (per!=passedlen*100/fileSize) {
                    per=passedlen*100/fileSize;
                    SendMsg("!@#$%file:send^&*文件接收了:" + per + "%");
                }
                //如果已经接到完文件，直接跳出，不while
                if (passedlen>=fileSize)//收到正确的流文件跳出
                    break;

                //流量控制
                flowControl(passedlen,beginTime,200);
            }
            SendMsg("!@#$%file:end^&*文件接收完毕");
            log.debug("======== 文件接收成功 耗时:"+(System.currentTimeMillis()-beginTime)/1000+"秒========");
        } catch (Exception e) {
            log.error("上传文件["+fileName+"]出错:"+e.toString());
            File directory = new File(uploadFilePath);
            File file = new File(directory.getAbsolutePath() + File.separatorChar + fileName);
            if (file!=null && file.isFile() && file.exists())
            {
                //上传文件失败，删除文件
                file.delete();
            }
            // e.printStackTrace();
        } finally {
            try {
                //可能会关闭socket注意后面调试测试
                if(fos != null)
                    fos.close();
                // if(dis != null)
                //     dis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    //#endregion

    //#region 外抛抽象事件

    /**
     * 抽象接收消息事件
     *
     * @param mag
     */
    public abstract void OnRevice(String mag);


    //#endregion

    //#region 发送消息及其命令

    /**
     * 客服端向服务端发送信息
     *
     * @param msg
     */
    public void SendMsg(String msg) {
        if (socket == null)
            return;
        //多线程，本实例内，全局锁定【socket】，排队轮流发送 消息 或文件
        synchronized(socket) {
            if (socket != null && socket.isConnected()) {
                OutputStream outputStream = null;

                // DataOutputStream dos = new DataOutputStream(outputStream); dataOutputStream = null;
                try {
                    outputStream = socket.getOutputStream();
                    //------------方法1--------------
                    msg += breakMark;
                    outputStream.write(msg.getBytes(StandardCharsets.UTF_8));
                    //-----------方法2--------------------
                    //由Socket对象得到输出流，并构造PrintWriter对象
                /*
                PrintWriter os=new PrintWriter(outputStream);
                if (breakMark.equals("\n"))
                    msg += "\n";
                else
                    msg += "\n" + breakMark;
                 os.write(msg);
                os.flush();

                 */
                    //----------方法3-----------------
                /*
                //注明：有限制 用dataOutputStream写入，要用 DataInputStream 读取，因为
                DataOutputStream dataOutputStream = null;
                //dataOutputStream 写入内容，带有文件类型识别前缀，需要DataInputStream识别
                dataOutputStream = new DataOutputStream(outputStream);
                if (breakMark.equals("\n"))
                    msg += "\n";
                else
                    msg += "\n" + breakMark;
                 dataOutputStream.writeUTF(msg);

                dataOutputStream.flush();
                */
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                /*
                if (outputStream!=null) {
                    try {
                        outputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }

                if (dataOutputStream!=null) {
                    try {
                        dataOutputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }*/
                }

            }

        }
    }


    /**
     * 发送文件到服务端
     *
     * @param fileName    文件名称
     * @param file    文件
     */
    public void SendFile(String fileName, File file) {
        if (file==null || !file.isFile() || !file.exists())
            return;
        OutputStream outputStream = null;
        InputStream file_inputStream = null;
        //多线程，本实例内，全局锁定【socket】，排队轮流发送 消息 或文件
        synchronized(socket) {
            if (socket != null && socket.isConnected()) {
                try {
                    outputStream = socket.getOutputStream();
                    //------------方法1--------------
                    //启动命令，消息转跳动作
                    SendMsg("!@#$%cmd:file:begin^&*");
                    //文件名称
                    SendMsg(fileName);
                    //文件大小
                    SendMsg(file.length() + "");
                    // 开始传输文件
                    log.debug("======== 开始传输文件 ========");
                    byte[] bytes = new byte[1024];
                    int length = 0;
                    long passedlen=0;
                    //统计开始时间
                    long beginTime=System.currentTimeMillis();
                    file_inputStream = new FileInputStream(file);
                    while ((length = file_inputStream.read(bytes, 0, bytes.length)) != -1) {
                        outputStream.write(bytes, 0, length);
                        outputStream.flush();

                        //流量控制
                        passedlen += length;
                        flowControl(passedlen, beginTime,200);
                    }
                    log.debug("======== 文件传输成功 耗时:"+(System.currentTimeMillis()-beginTime)/1000+"秒========");
                    //SendMsg("!@#$%cmd:file:end^&*");
                    // outputStream.write(msg.getBytes(StandardCharsets.UTF_8));
                } catch (IOException e) {
                    log.error("上传文件错误:" + e.toString());
                    //e.printStackTrace();
                }
                finally {
                    if (file_inputStream != null) {
                        try {
                            file_inputStream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }

                /*
                不能关闭，关闭整个socket就关了
                if (outputStream != null) {
                    try {
                        outputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }

                 */
                }

            }
        }
    }

    //#endregion

    //#region 线程同步 及其他

    /**
     * 流量控制  (目前策略 接收是 发送 2 被 限流 ，双方需要约定好 ）
     * 这是接收端端限制，因为是并发异步/同步，所以不能同步阻止发送端写速（NIO/BIO）
     * 只能总限制 并发总数 * 单并发 = 总速度 , 例如 ： 限制 200KB * 100 = 20 M / S  ,1000并发 200M， 1W并发 2000M
     * @param passedlen 总已传输字节 (b！,非KB）
     * @param beginTime 开始时间
     * @param limitspped 限制速度  kb / s （200 = 200 kb/s = 2k / ms)
     */
    private void flowControl(long passedlen ,long beginTime,long limitspped)
    {
        //流量控制 100KB/S = 100  * 1024  b/s  = 1024000 b/S =  102 b /ms
        //limitspped KB /s = limitspped * 1024 * 1000 / 1000 = limitspped * 1024  / ms
        long passTime = (System.currentTimeMillis() - beginTime);
        if(passTime==0) passTime=1;
        long pp = passedlen/limitspped - passTime ;//如果passTime=0，按实际所需passedlen/limitspped等待
        if (pp > 0) {
            try {
                Thread.sleep(pp);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 去除字符串后缀
     * @param msg
     * @return
     */
    private String delSuffixMsg(String msg) {
        if (msg.endsWith(breakMark)) {
            msg= msg.substring(0,msg.length()-breakMark.length());
        }
        return msg;
    }
    //#endregion
}
